/* NOTICE
 
    Jim - the Java Image Manager, copyright (c) 2005 - 2007 Grant Gardner 
 
    Jim is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Jim is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
  
    In addition, as a special exception, you are permitted to link this code
    with the 3rd party libraries required to compile this file and to
    distribute a linked combination including them. You must obey the
    GNU General Public License in all respects for all of the code except for
    those 3rd party libraries that are licenced under terms incompatible with the
    GNU General Public License.  
 
 
 NOTICE */

/*
 * EXIFReader.java
 * Based on some public domain software...
 * ExifReader.java from Drew Noakes (http://drewnoakes.com)
 * In turn that was based upon code from Jhead
 *   http://www.sentex.net/~mwandel/jhead/
 *
 */

package au.com.lastweekend.jim.io.exif;

import org.apache.log4j.Logger;

import au.com.lastweekend.jim.io.metadata.Directory;

/**
 * 
 * Makes ExifDirectory from raw data Everything this class does must be reversible.
 * 
 * @version $Id: AbstractTiffTagReader.java,v 1.2 2005/06/09 11:15:37 ggardner Exp $
 * @author <a href="mailto:grant@lastweekend.com.au"> <Grant Gardner> </a>
 */
public abstract class AbstractTiffTagReader {

    private static final Logger LOG = Logger.getLogger(AbstractTiffTagReader.class);

    private int startOffset;

    private TiffTagFormat dataFormat;

    /**
     * The raw Exif data
     */
    private final byte[] data;

    private int tagCount = 0;
    private Directory directory = null;
    private int linkedDirectoryOffset = 0;

    public AbstractTiffTagReader(byte[] data) {

        this.data = data;
    }

    /**
     * Process tags contained in TIFF tag format. Generally you'd expect to call processTiffHeader() or processTiffTags() in here
     * and to return a new directory instance of the appropriate type.
     * 
     * @param parentDirectory
     * @return the extracted Directory
     * @throws TiffTagProcessingException
     */
    public abstract Directory extract(Directory parentDirectory) throws TiffTagProcessingException;

    public final void processTiffTags() throws TiffTagProcessingException {

        int startOffset = getStartOffset();
        if (LOG.isDebugEnabled()) {
            LOG.debug("Reading tiff tags at offset: " + startOffset);
        }

        if (startOffset >= data.length || startOffset < 0) {
            throw new TiffTagProcessingException("Ignored directory marked to start outside data segement");
        }
        setTagCount();
        // Check directory length
        checkValidDirectoryLength();
        // For each Tag...
        for (int dirEntry = 0; dirEntry < tagCount; dirEntry++) {
            int dirEntryOffset = startOffset + 2 + (12 * dirEntry);
            int tagType = dataFormat.get16Bits(data, dirEntryOffset);
            int formatCode = dataFormat.get16Bits(data, dirEntryOffset + 2);
            if (formatCode < 0 || formatCode > TiffTagFormat.MAX_FORMAT_CODE) {
                throw new TiffTagProcessingException("Invalid format code: " + formatCode);
            }
            // 4 bytes indicating number of formatCode type data for this tag
            // So if it is a string, the component count is the number of bytes.
            // If there are more than 4 bytes, then we get an offset.
            int componentCount = dataFormat.get32Bits(data, dirEntryOffset + 4);
            int tagValueOffset = calculateTagValueOffset(dirEntryOffset, formatCode, componentCount);
            if (tagValueOffset < 0) {
                throw new TiffTagProcessingException("Illegal pointer offset value in EXIF");
            }
            processTag(tagType, formatCode, componentCount, tagValueOffset);
        }
        setLinkedDirectoryOffset();
        AbstractTiffTagReader linkedReader = getLinkedDirectoryReader();
        if (linkedReader != null) {
            if (getDirectory().hasLinkedDirectory()) {
                throw new TiffTagProcessingException("Not expecting another linked directory");
            }

            Directory linkedDirectory = linkedReader.extract(getDirectory());
            getDirectory().setLinkedDirectory(linkedDirectory);
        }
    }

    /**
     * @return a new Tiff reader to read the linked directory.
     */
    protected abstract AbstractTiffTagReader getLinkedDirectoryReader();

    /**
     * Sets the tag count
     */
    private void setTagCount() {

        tagCount = dataFormat.get16Bits(data, getStartOffset());
    }

    /**
     * check directory length is valid
     */
    private void checkValidDirectoryLength() throws TiffTagProcessingException {

        int dirLength = (2 + (12 * tagCount) + 4);
        if (tagCount > 0 && dirLength + getStartOffset() >= data.length) {
            throw new TiffTagProcessingException(tagCount + " tags, from offset " + getStartOffset() + "exceeds data length "
                    + data.length);
        }
    }

    /**
     * sets the linked directory location
     */
    private void setLinkedDirectoryOffset() {

        // At the end of each IFD is an optional link to the next IFD. This link
        // is after
        // the 2-byte tag count, and after 12 bytes for each of these tags,
        // hence
        linkedDirectoryOffset = dataFormat.get32Bits(data, getStartOffset() + 2 + 12 * tagCount);
        if (linkedDirectoryOffset != 0) {
            if (linkedDirectoryOffset >= data.length) {
                linkedDirectoryOffset = 0;
            }
        }
        if (LOG.isDebugEnabled() && linkedDirectoryOffset > 0) {
            LOG.debug("Linked directory found at: " + linkedDirectoryOffset);
        }
    }

    /**
     * Standard Tag processing.
     * 
     * @param tagType
     * @param formatCode
     * @param componentCount
     * @param tagValueOffset
     */
    protected void processTag(int tagType, int formatCode, int componentCount, int tagValueOffset)
            throws TiffTagProcessingException {

        int byteCount = dataFormat.getByteCount(formatCode, componentCount);
        if (tagValueOffset + byteCount > data.length) {
            throw new TiffTagProcessingException("Tag length exceeds data length");
        }
        Object tagValue = dataFormat.parse(data, formatCode, componentCount, tagValueOffset);
        directory.setTag(tagType, formatCode, tagValue);
    }

    private int calculateTagValueOffset(int dirEntryOffset, int formatCode, int componentCount) {

        int byteCount = dataFormat.getByteCount(formatCode, componentCount);
        if (byteCount > 4) {
            // If its bigger than 4 bytes, the dir entry contains an offset.
            // @TODO if we're reading FujiFilm makernote tags, the offset is
            // relative to the start of the makernote itself, not the TIFF
            // segment
            int offsetVal = dataFormat.get32Bits(data, dirEntryOffset + 8);

            if (LOG.isDebugEnabled()) {
                LOG.debug("Long tag value found at: " + offsetVal);
            }

            return offsetVal;
        } else {
            // 4 bytes or less and value is in the dir entry itself
            return dirEntryOffset + 8;
        }
    }

    public int getStartOffset() {

        return startOffset;
    }

    /**
     * @param i
     */
    protected void setStartOffset(int startOffset) {

        this.startOffset = startOffset;
    }

    public Directory getDirectory() {

        return directory;
    }

    /**
     * @param directory
     */
    public void setDirectory(Directory directory) {

        this.directory = directory;
    }

    public byte[] getData() {

        return data;
    }

    public TiffTagFormat getDataFormat() {

        return dataFormat;
    }

    public int getLinkedDirectoryOffset() {

        return linkedDirectoryOffset;
    }

    /**
     * @param format
     */
    public void setDataFormat(TiffTagFormat format) {

        dataFormat = format;
    }

    /**
     * Add a sub directory
     * 
     * @param reader
     * @param tagType
     * @param formatCode
     * @param componentCount
     * @param tagValueOffset
     * @throws MetadataException
     * @throws TiffTagProcessingException
     */
    protected final void processSubDirectory(AbstractTiffTagReader reader, int tagType, int formatCode, int componentCount,
            int tagValueOffset) throws TiffTagProcessingException {

        // Value is an offset
        int subdirOffset = getDataFormat().get32Bits(getData(), tagValueOffset);
        reader.setStartOffset(subdirOffset);
        reader.setDataFormat(getDataFormat());
        Directory subDirectory = reader.extract(getDirectory());
        getDirectory().addSubDirectory(tagType, formatCode, subDirectory);
    }

    /**
     * Checks if Tiff Header is valid
     */
    protected final void processTiffHeader() throws TiffTagProcessingException {

        byte[] data = getData();
        // Exif data is at least 14 long.
        if (data.length <= 8) {
            throw new TiffTagProcessingException("Invalid tiff header, expecting length > 8, length is " + data.length);
        }
        // this should be either "MM" or "II"
        String byteOrderIdentifier = new String(data, 0, 2);
        boolean isMotorolaByteOrder = getByteOrder(byteOrderIdentifier);
        setDirectoryByteOrder(isMotorolaByteOrder);
        setDataFormat(TiffTagFormat.getInstance(isMotorolaByteOrder));
        // Check the next two values for correctness.
        int magicNumber = getDataFormat().get16Bits(data, 2);
        if (magicNumber != ExifDirectory.EXIF_DIRECTORY_IDENTIFIER) {
            throw new TiffTagProcessingException("Invalid tiff header, expecting " + ExifDirectory.EXIF_DIRECTORY_IDENTIFIER
                    + "got " + magicNumber);
        }
        int startOffset = getDataFormat().get32Bits(data, 4);
        // David Ekholm sent an digital camera image that has this problem
        if (startOffset >= data.length - 1) {
            LOG.warn("Invalid 0th IFD offset (" + startOffset + "), using 8 instead");
            startOffset = 8;
        }
        if (startOffset != 8) {
            LOG.warn("Startoffset!=8, possibly skipping some metadata");
        }
        setStartOffset(startOffset);
    }

    /**
     * Do something here in a subclass if you care about byte order
     * 
     * @param isMotorolaByteOrder
     */
    protected void setDirectoryByteOrder(boolean isMotorolaByteOrder) {

        return;
    }

    /**
     * @param byteOrderIdentifier
     */
    private boolean getByteOrder(String byteOrderIdentifier) throws TiffTagProcessingException {

        if ("MM".equals(byteOrderIdentifier)) {
            return true;
        } else if ("II".equals(byteOrderIdentifier)) {
            return false;
        }
        throw new TiffTagProcessingException("Invalid byte order identifier: " + byteOrderIdentifier);
    }
}